package com.hujun.modules.product.service;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.DefaultScriptExecutor;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.hujun.modules.product.api.ProductClassifyService;
import com.hujun.modules.product.domain.ProductClassify;
import com.hujun.modules.product.peresistence.ProductClassifyMapper;

/**
 * 商品分类实现类
 * @author huj
 *
 */
@Service
public class ProductClassifyServiceImpl implements ProductClassifyService {

	private Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ProductClassifyMapper productClassifyMapper;
	@Autowired
	private StringRedisTemplate redisTemplate;
	@Autowired
	private RedissonClient redissonClient;

	/**
	 * 查询商品分类数据
	 * @return
	 */
	@Override
	public List<ProductClassify> findProductClassifyListMysqlV1() {
		logger.debug("查询了数据库......");
		// 先查出全部分类
		List<ProductClassify> allList = productClassifyMapper.selectListByMap(new HashMap<>());
		// 取出一级分类
		List<ProductClassify> list = allList.stream().filter(a -> a.getParentId().equals(Long.valueOf("0"))).collect(Collectors.toList());
		// 处理子分类
		list.stream().forEach(item -> {
			recursion(item, allList);
		});
		return list;
	}

	/**
	 * @apiNote
	 * 目前存在问题，高并发下，缓存穿透、雪崩、击穿的问题
	 * 如果解决？
	 * 1.空结果缓存，解决缓存穿透
	 * 2.设置过期时间（加随机值），解决缓存雪崩
	 * 3.加锁，解决缓存击穿
	 */
	@Override
	public List<ProductClassify> findProductClassifyListRedisV2() {
		// 给缓存中放json字符串，拿出的json字符串，还要逆转为能用的对象类型；【序列化与反序列化】
		// 1.加入缓存逻辑,缓存中存的数据是json字符串
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		// 2.如果缓存中没有，查询数据库
		if (StringUtils.isEmpty(catalogJSON)) {
			// 3.查询到的数据库再放入缓存
			logger.debug("缓存不命中......查询数据库......" + Thread.currentThread().getId());
			List<ProductClassify> list = findProductClassifyListMysqlV1();
			String s = JSON.toJSONString(list);
			redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
			return list;
		}
		logger.debug("缓存命中......直接返回......");
		// 转为指定的对象
		List<ProductClassify> resultList = JSON.parseObject(catalogJSON, new TypeReference<List<ProductClassify>>() {});
		return resultList;
	}

	/**
	 * 获取商品分类（Synchronized加锁版，单体服务是没有问题的，但分布式下高并发时存在问题）
	 * @return
	 * @apiNote
	 * 只要是同一把锁，就能锁住需要这个锁的所有线程
	 * 1、synchronized (this)：springboot中所有的组件在容器中都是单例的
	 * TODO 本地锁：synchronized，JUC(Lock)。在分布式情况下，想要锁住所有，必须使用分布式锁
	 */
	@Override
	public List<ProductClassify> findProductClassifyListBySynchronized() {
		synchronized (this) {
			return findProductClassifyListRedisV2();
		}
	}

	@Override
	public List<ProductClassify> findProductClassifyListByRedisLock() {
		// 1.占分布式锁。去redis占坑
		String uuid = UUID.randomUUID().toString();
		Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
		if (lock) {
			// 加锁成功...执行业务
			logger.debug("获取分布式锁成功...");
			// 2.设置过期时间，必须和加锁同步（即原子的）
			List<ProductClassify> dataFromDb;
			try {
				dataFromDb = getDataFromDb();
			} finally {
				String script = "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
						"then\n" +
						"    return redis.call('del',KEYS[1])\n" +
						"else\n" +
						"    return 0\n" +
						"end";;
				// 删除锁
				Long lock1 = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
			}
			return dataFromDb;
		} else {
			// 加锁失败...重试
			logger.debug("获取分布式锁失败...等待重试");
			// 休眠200ms重试
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return findProductClassifyListByRedisLock(); // 自旋的方式
		}
	}

	/**
	 *
	 * @return
	 * @apiNote
	 * 缓存里面的数据如何和数据库中的保持一致？
	 * 缓存数据一致性问题
	 * 1、双写模式
	 * 2、失效模式
	 */
	@Override
	public List<ProductClassify> findProductClassifyListByRedissonLock() {
		// 1、锁的名字。锁的粒度，越细越快
		// 锁的粒度：具体缓存的是某个具体的数据， 11-号商品 => product-11-lock product-12-lock
		RLock lock = redissonClient.getLock("catalogJSON-lock");
		lock.lock();
		List<ProductClassify> dataFromDb;
		try {
			dataFromDb = getDataFromDb();
		} finally {
			lock.unlock();
		}
		return dataFromDb;
	}

	private List<ProductClassify> getDataFromDb() {
		// 如果缓存中有，直接返回
		String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
		if (!StringUtils.isEmpty(catalogJSON)) {
			List<ProductClassify> resultList = JSON.parseObject(catalogJSON, new TypeReference<List<ProductClassify>>() {});
			return resultList;
		}
		logger.debug("查询了数据库......");
		List<ProductClassify> classifyList = findProductClassifyListMysqlV1();
		String s = JSON.toJSONString(classifyList);
		redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
		return classifyList;
	}

	/**
	 * 递归处理
	 * @param t
	 * @param allList
	 */
	private void recursion(ProductClassify t, List<ProductClassify> allList) {
		if (t == null || allList == null || allList.size() == 0) {
			return;
		}
		// 取出当前节点的子节点
		List<ProductClassify> nodeList = allList.stream().filter(a -> a.getParentId().equals(t.getClassifyId())).collect(Collectors.toList());
		if (nodeList != null && nodeList.size() > 0) {
			// 设置子节点
			t.setChildren(nodeList);
			// 遍历子节点，以子节点作为父节点，又去取出子节点的子节点
			nodeList.stream().forEach(node -> {
				recursion(node, allList);
			});
		}
	}



}
